using System;
using UnityEngine.Experimental.Rendering;
using System.Collections.Generic;

namespace UnityEngine.Rendering.HighDefinition
{
    class PlanarReflectionProbeCache
    {
        internal static readonly int s_InputTexID = Shader.PropertyToID("_InputTex");
        internal static readonly int s_LoDID = Shader.PropertyToID("_LoD");
        internal static readonly int s_FaceIndexID = Shader.PropertyToID("_FaceIndex");

        enum ProbeFilteringState
        {
            Convolving,
            Ready
        }

        int                     m_ProbeSize;
        IBLFilterGGX            m_IBLFilterGGX;
        PowerOfTwoTextureAtlas  m_TextureAtlas;
        RenderTexture           m_TempRenderTexture = null;
        RenderTexture           m_ConvolutionTargetTexture;
        Dictionary<Vector4, ProbeFilteringState> m_ProbeBakingState = new Dictionary<Vector4, ProbeFilteringState>();
        Material                m_ConvertTextureMaterial;
        MaterialPropertyBlock   m_ConvertTextureMPB;
        Dictionary<int, uint>   m_TextureHashes = new Dictionary<int, uint>();
        int                     m_FrameProbeIndex;
        GraphicsFormat          m_ProbeFormat;

        public PlanarReflectionProbeCache(RenderPipelineResources defaultResources, IBLFilterGGX iblFilter, int atlasResolution, GraphicsFormat probeFormat, bool isMipmaped)
        {
            m_ConvertTextureMaterial = CoreUtils.CreateEngineMaterial(defaultResources.shaders.blitCubeTextureFacePS);
            m_ConvertTextureMPB = new MaterialPropertyBlock();

            m_ProbeSize = atlasResolution;
            m_TextureAtlas = new PowerOfTwoTextureAtlas(atlasResolution, 0, probeFormat, useMipMap: isMipmaped, name: "PlanarReflectionProbe Atlas");
            m_IBLFilterGGX = iblFilter;

            m_ProbeFormat = probeFormat;
        }

        void Initialize()
        {
            if (m_ConvolutionTargetTexture == null)
            {
                m_ConvolutionTargetTexture = new RenderTexture(m_ProbeSize, m_ProbeSize, 0, m_ProbeFormat);
                m_ConvolutionTargetTexture.hideFlags = HideFlags.HideAndDontSave;
                m_ConvolutionTargetTexture.dimension = TextureDimension.Tex2D;
                m_ConvolutionTargetTexture.useMipMap = true;
                m_ConvolutionTargetTexture.autoGenerateMips = false;
                m_ConvolutionTargetTexture.filterMode = FilterMode.Point;
                m_ConvolutionTargetTexture.name = CoreUtils.GetRenderTargetAutoName(m_ProbeSize, m_ProbeSize, 0, m_ProbeFormat, "PlanarReflectionConvolution", mips: true);
                m_ConvolutionTargetTexture.enableRandomWrite = true;
                m_ConvolutionTargetTexture.Create();

                // Clear to avoid garbage in the convolution texture.
                int mipCount = Mathf.FloorToInt(Mathf.Log(m_ProbeSize, 2)) + 1;
                for (int mipIdx = 0; mipIdx < mipCount; ++mipIdx)
                {
                    Graphics.SetRenderTarget(m_ConvolutionTargetTexture, mipIdx, CubemapFace.Unknown);
                    GL.Clear(false, true, Color.clear);
                }

            }

            m_FrameProbeIndex = 0;
        }

        public void Release()
        {
            m_TextureAtlas.Release();
            CoreUtils.Destroy(m_TempRenderTexture);
            CoreUtils.Destroy(m_ConvolutionTargetTexture);

            m_ProbeBakingState = null;

            CoreUtils.Destroy(m_ConvertTextureMaterial);
        }

        public void NewFrame()
        {
            Initialize();
        }

        // This method is used to convert inputs that are either compressed or not of the right size.
        // We can't use Graphics.ConvertTexture here because it does not work with a RenderTexture as destination.
        void ConvertTexture(CommandBuffer cmd, Texture input, RenderTexture target)
        {
            m_ConvertTextureMPB.SetTexture(s_InputTexID, input);
            m_ConvertTextureMPB.SetFloat(s_LoDID, 0.0f); // We want to convert mip 0 to whatever the size of the destination cache is.
            CoreUtils.SetRenderTarget(cmd, target, ClearFlag.None, Color.black, 0, 0);
            CoreUtils.DrawFullScreen(cmd, m_ConvertTextureMaterial, m_ConvertTextureMPB);
        }

        Texture ConvolveProbeTexture(CommandBuffer cmd, Texture texture, ref IBLFilterBSDF.PlanarTextureFilteringParameters planarTextureFilteringParameters, out Vector4 sourceScaleOffset)
        {
            Texture2D texture2D = texture as Texture2D;
            RenderTexture renderTexture = texture as RenderTexture;

            if (renderTexture.dimension != TextureDimension.Tex2D)
            {
                Debug.LogError("Planar Realtime reflection probe should always be a 2D RenderTexture.");
                sourceScaleOffset = Vector4.zero;
                return null;
            }

            float scaleX = (float)texture.width / m_ConvolutionTargetTexture.width;
            float scaleY = (float)texture.height / m_ConvolutionTargetTexture.height;
            sourceScaleOffset = new Vector4(scaleX, scaleY, 0, 0);
            m_IBLFilterGGX.FilterPlanarTexture(cmd, renderTexture, ref planarTextureFilteringParameters, m_ConvolutionTargetTexture);

            return m_ConvolutionTargetTexture;
        }

        public Vector4 FetchSlice(CommandBuffer cmd, Texture texture, ref IBLFilterBSDF.PlanarTextureFilteringParameters planarTextureFilteringParameters, out int fetchIndex)
        {
            Vector4 scaleOffset = Vector4.zero;
            fetchIndex = m_FrameProbeIndex++;

            if (m_TextureAtlas.IsCached(out scaleOffset, texture))
            {
                // If the texture is already in the atlas, we update it only if needed
                if (NeedsUpdate(texture) || m_ProbeBakingState[scaleOffset] != ProbeFilteringState.Ready)
                    if (!UpdatePlanarTexture(cmd, texture, ref planarTextureFilteringParameters, ref scaleOffset))
                        Debug.LogError("Can't convolve or update the planar reflection render target");
            }
            else // Either we add it to the atlas
                if (!UpdatePlanarTexture(cmd, texture, ref planarTextureFilteringParameters, ref scaleOffset))
                    Debug.LogError("No more space in the planar reflection probe atlas. To solve this issue, increase the size of the Planar Reflection Probe Atlas in the HDRP settings.");

            return scaleOffset;
        }

        bool UpdatePlanarTexture(CommandBuffer cmd, Texture texture, ref IBLFilterBSDF.PlanarTextureFilteringParameters planarTextureFilteringParameters, ref Vector4 scaleOffset)
        {
            bool    success = false;

            using (new ProfilingScope(cmd, ProfilingSampler.Get(HDProfileId.ConvolvePlanarReflectionProbe)))
            {
                m_ProbeBakingState[scaleOffset] = ProbeFilteringState.Convolving;

                Vector4 sourceScaleOffset;
                Texture convolvedTexture = ConvolveProbeTexture(cmd, texture, ref planarTextureFilteringParameters, out sourceScaleOffset);
                if (convolvedTexture == null)
                    return false;

                if (m_TextureAtlas.IsCached(out scaleOffset, texture))
                {
                    success = m_TextureAtlas.UpdateTexture(cmd, texture, convolvedTexture, ref scaleOffset, sourceScaleOffset);
                }
                else
                {
                    // Reserve space for the rendertarget and then blit the result of the convolution at this
                    // location, we don't use the UpdateTexture because it will keep the reference to the
                    // temporary target used to convolve the result of the probe rendering.
                    if (!m_TextureAtlas.AllocateTextureWithoutBlit(texture, texture.width, texture.height, ref scaleOffset))
                        return false;
                    m_TextureAtlas.BlitTexture(cmd, scaleOffset, convolvedTexture, sourceScaleOffset);
                    success = true;
                }

                m_ProbeBakingState[scaleOffset] = ProbeFilteringState.Ready;
            }

            return success;
        }

        public uint GetTextureHash(Texture texture)
        {
            uint textureHash  = texture.updateCount;
            // For baked probes in the editor we need to factor in the actual hash of texture because we can't increment the update count of a texture that's baked on the disk.
#if UNITY_EDITOR
            textureHash += (uint)texture.imageContentsHash.GetHashCode();
#endif
            return textureHash;
        }

        bool NeedsUpdate(Texture texture)
        {
            uint savedTextureHash;
            uint currentTextureHash = GetTextureHash(texture);
            int instanceId = texture.GetInstanceID();
            bool needsUpdate = false;

            if (!m_TextureHashes.TryGetValue(instanceId, out savedTextureHash) || savedTextureHash != currentTextureHash)
            {
                m_TextureHashes[instanceId] = currentTextureHash;
                needsUpdate = true;
            }

            return needsUpdate;
        }

        public Texture GetTexCache() => m_TextureAtlas.AtlasTexture;


        public void Clear(CommandBuffer cmd)
        {
            m_TextureAtlas.ResetAllocator();
            m_TextureAtlas.ClearTarget(cmd);
        }

        public void ClearAtlasAllocator() => m_TextureAtlas.ResetAllocator();

        internal static long GetApproxCacheSizeInByte(int nbElement, int atlasResolution, GraphicsFormat format)
            => PowerOfTwoTextureAtlas.GetApproxCacheSizeInByte(nbElement, atlasResolution, true, format);

        internal static int GetMaxCacheSizeForWeightInByte(int weight, GraphicsFormat format)
            => PowerOfTwoTextureAtlas.GetMaxCacheSizeForWeightInByte(weight, true, format);
        
        internal Vector4 GetAtlasDatas()
        {
            float padding = Mathf.Pow(2.0f, m_TextureAtlas.mipPadding) * 2.0f;

            return new Vector4(
                m_TextureAtlas.AtlasTexture.rt.width,
                padding / (float)m_TextureAtlas.AtlasTexture.rt.width,
                0,
                0
            );
        }
    }
}
